﻿<!--
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../resources/js-test-style.css"/>
<script src="../../js/js-test-pre.js"></script>
<script src="../../js/webgl-test-utils.js"></script>
<script src="../../js/tests/compressed-texture-utils.js"></script>
<title>WebGL WEBGL_compressed_texture_s3tc and EXT_texture_compression_rgtc Conformance Tests</title>
<style>
img {
 border: 1px solid black;
 margin-right: 1em;
}

.testimages br {
  clear: both;
}

.testimages > div {
  float: left;
  margin: 1em;
}
</style>
</head>
<body>
<div id="description"></div>
<canvas id="canvas" width="8" height="8" style="width: 8px; height: 8px;"></canvas>
<div id="console"></div>
<script>
"use strict";
description("This test verifies the functionality of the WEBGL_compressed_texture_s3tc extension, if it is available. It also tests the related formats from the EXT_texture_compression_rgtc extension.");

debug("");

// Acceptable interpolation error depends on endpoints:
// 1.0 / 255.0 + 0.03 * max(abs(endpoint0 - endpoint1), abs(endpoint0_p - endpoint1_p))
// For simplicity, assume the worst case (e0 is 0.0, e1 is 1.0). After conversion to unorm8, it is 9.
const DEFAULT_COLOR_ERROR = 9;

/*
BC1 (DXT1) block
e0 = [  0, 255, 0]
e1 = [255,   0, 0]
e0 < e1, so it uses 3-color mode

local palette
    0: [  0, 255, 0, 255]
    1: [255,   0, 0, 255]
    2: [128, 128, 0, 255]
    3: [  0,   0, 0, 255] // for BC1 RGB
    3: [  0,   0, 0,   0] // for BC1 RGBA
selectors
    3 2 1 0
    2 2 1 0
    1 1 1 0
    0 0 0 0

Extending this block with opaque alpha and uploading as BC2 or BC3
will generate wrong colors because BC2 and BC3 do not have 3-color mode.
*/
var img_4x4_rgba_dxt1 = new Uint8Array([
    0xE0, 0x07, 0x00, 0xF8, 0x1B, 0x1A, 0x15, 0x00
]);

/*
BC2 (DXT3) block

Quantized alpha values
    0 1 2 3
    4 5 6 7
    8 9 A B
    C D E F

RGB block
e0 = [255,   0, 0]
e1 = [  0, 255, 0]
BC2 has only 4-color mode

local palette
    0: [255,   0, 0]
    1: [  0, 255, 0]
    2: [170,  85, 0]
    3: [ 85, 170, 0]
selectors
    0 1 2 3
    1 1 2 3
    2 2 2 3
    3 3 3 3
*/
var img_4x4_rgba_dxt3 = new Uint8Array([
    0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
    0x00, 0xF8, 0xE0, 0x07, 0xE4, 0xE5, 0xEA, 0xFF
]);

/*
BC3 (DXT5) block

Alpha block (aka DXT5A)
e0 = 255
e1 = 0
e0 > e1, so using 6 intermediate points
local palette
    255, 0, 219, 182, 146, 109, 73, 36
selectors
    0 1 2 3
    1 2 3 4
    2 3 4 5
    3 4 5 6

RGB block
e0 = [255,   0, 0]
e1 = [  0, 255, 0]
BC3 has only 4-color mode

local palette
    0: [255,   0, 0]
    1: [  0, 255, 0]
    2: [170,  85, 0]
    3: [ 85, 170, 0]
selectors
    3 2 1 0
    3 2 1 1
    3 2 2 2
    3 3 3 3
*/
var img_4x4_rgba_dxt5 = new Uint8Array([
    0xFF, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x00, 0xF8, 0xE0, 0x07, 0x1B, 0x5B, 0xAB, 0xFF
]);

// BC4 - just the alpha block from BC3 above, interpreted as the red channel.
// See http://www.reedbeta.com/blog/understanding-bcn-texture-compression-formats/#bc4
// for format details.
var img_4x4_r_bc4 = new Uint8Array([
    0xFF, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);

// BC5 - Two BC3 alpha blocks, interpreted as the red and green channels.
var img_4x4_rg_bc5 = new Uint8Array([
    0xFF, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x00, 0xFF, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);

// Signed BC4 - change endpoints to use full -1 to 1 range.
var img_4x4_signed_r_bc4 = new Uint8Array([
    0x7F, 0x80, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);

// Signed BC5 - Two BC3 alpha blocks, interpreted as the red and green channels.
var img_4x4_signed_rg_bc5 = new Uint8Array([
    0x7F, 0x80, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x80, 0x7F, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);


/*
8x8 block endpoints use half-intensity values (appear darker than 4x4)
*/
var img_8x8_rgba_dxt1 = new Uint8Array([
    0xe0,0x03,0x00,0x78,0x13,0x10,0x15,0x00,
    0x0f,0x00,0xe0,0x7b,0x11,0x10,0x15,0x00,
    0xe0,0x03,0x0f,0x78,0x44,0x45,0x40,0x55,
    0x0f,0x00,0xef,0x03,0x44,0x45,0x40,0x55
]);
var img_8x8_rgba_dxt3 = new Uint8Array([
    0xf6,0xff,0xf6,0xff,0xff,0xff,0xff,0xff,0x00,0x78,0xe0,0x03,0x44,0x45,0x40,0x55,
    0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x7b,0x0f,0x00,0x44,0x45,0x40,0x55,
    0xff,0xff,0xff,0xff,0xf6,0xff,0xf6,0xff,0x0f,0x78,0xe0,0x03,0x11,0x10,0x15,0x00,
    0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xef,0x03,0x0f,0x00,0x11,0x10,0x15,0x00
]);
var img_8x8_rgba_dxt5 = new Uint8Array([
    0xff,0x69,0x01,0x10,0x00,0x00,0x00,0x00,0x00,0x78,0xe0,0x03,0x44,0x45,0x40,0x55,
    0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0x7b,0x0f,0x00,0x44,0x45,0x40,0x55,
    0xff,0x69,0x00,0x00,0x00,0x01,0x10,0x00,0x0f,0x78,0xe0,0x03,0x11,0x10,0x15,0x00,
    0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0xef,0x03,0xef,0x00,0x11,0x10,0x15,0x00
]);
var img_8x8_r_bc4 = new Uint8Array([
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);
var img_8x8_rg_bc5 = new Uint8Array([
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6, 0x00, 0x7F, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6, 0x00, 0x7F, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6, 0x00, 0x7F, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
    0x7F, 0x00, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6, 0x00, 0x7F, 0x88, 0x16, 0x8D, 0x1A, 0x3B, 0xD6,
]);

var wtu = WebGLTestUtils;
var ctu = CompressedTextureUtils;
var contextVersion = wtu.getDefault3DContextVersion();
var canvas = document.getElementById("canvas");
var gl = wtu.create3DContext(canvas, {antialias: false});
var program = wtu.setupTexturedQuad(gl);
var ext = null;
var ext_rgtc = {};
var vao = null;
var validFormats = {
    COMPRESSED_RGB_S3TC_DXT1_EXT  : 0x83F0,
    COMPRESSED_RGBA_S3TC_DXT1_EXT : 0x83F1,
    COMPRESSED_RGBA_S3TC_DXT3_EXT : 0x83F2,
    COMPRESSED_RGBA_S3TC_DXT5_EXT : 0x83F3,
};
var name;
var supportedFormats;

if (!gl) {
    testFailed("WebGL context does not exist");
} else {
    testPassed("WebGL context exists");

    // Run tests with extension disabled
    ctu.testCompressedFormatsUnavailableWhenExtensionDisabled(gl, validFormats, expectedByteLength, 4);

    // Query the extension and store globally so shouldBe can access it
    ext = wtu.getExtensionWithKnownPrefixes(gl, "WEBGL_compressed_texture_s3tc");
    if (!ext) {
        testPassed("No WEBGL_compressed_texture_s3tc support -- this is legal");
        wtu.runExtensionSupportedTest(gl, "WEBGL_compressed_texture_s3tc", false);
    } else {
        testPassed("Successfully enabled WEBGL_compressed_texture_s3tc extension");

        wtu.runExtensionSupportedTest(gl, "WEBGL_compressed_texture_s3tc", true);
        runTestExtension();
    }
    ext_rgtc = wtu.getExtensionWithKnownPrefixes(gl, "EXT_texture_compression_rgtc");
    if (ext_rgtc) {
        ext = ext || {};
        // Make ctu.formatToString work for rgtc enums.
        for (const name in ext_rgtc)
            ext[name] = ext_rgtc[name];
        runTestRGTC();
    }
}

function expectedByteLength(width, height, format) {
    if (format == validFormats.COMPRESSED_RGBA_S3TC_DXT3_EXT || format == validFormats.COMPRESSED_RGBA_S3TC_DXT5_EXT) {
        return Math.floor((width + 3) / 4) * Math.floor((height + 3) / 4) * 16;
    }
    return Math.floor((width + 3) / 4) * Math.floor((height + 3) / 4) * 8;
}

function getBlockDimensions(format) {
    return {width: 4, height: 4};
}

function runTestExtension() {
    debug("");
    debug("Testing WEBGL_compressed_texture_s3tc");

    // Test that enum values are listed correctly in supported formats and in the extension object.
    ctu.testCompressedFormatsListed(gl, validFormats);
    ctu.testCorrectEnumValuesInExt(ext, validFormats);
    // Test that texture upload buffer size is validated correctly.
    ctu.testFormatRestrictionsOnBufferSize(gl, validFormats, expectedByteLength, getBlockDimensions);

    // Test each format
    testDXT1_RGB();
    testDXT1_RGBA();
    testDXT3_RGBA();
    testDXT5_RGBA();

    // Test compressed PBOs with a single format
    if (contextVersion >= 2) {
        testDXT5_RGBA_PBO();
    }

    // Test TexImage validation on level dimensions combinations.
    debug("");
    debug("When level equals 0, width and height must be a multiple of 4.");
    debug("When level is larger than 0, this constraint doesn't apply.");
    ctu.testTexImageLevelDimensions(gl, ext, validFormats, expectedByteLength, getBlockDimensions,
      [
        { level: 0, width: 4, height: 3, expectation: gl.INVALID_OPERATION, message: "0: 4x3" },
        { level: 0, width: 3, height: 4, expectation: gl.INVALID_OPERATION, message: "0: 3x4" },
        { level: 0, width: 2, height: 2, expectation: gl.INVALID_OPERATION, message: "0: 2x2" },
        { level: 0, width: 4, height: 4, expectation: gl.NO_ERROR, message: "0: 4x4" },
        { level: 1, width: 2, height: 2, expectation: gl.NO_ERROR, message: "1: 2x2" },
        { level: 2, width: 1, height: 1, expectation: gl.NO_ERROR, message: "2: 1x1" },
    ]);

    ctu.testTexSubImageDimensions(gl, ext, validFormats, expectedByteLength, getBlockDimensions, 16, 16,
      [
        { xoffset: 0, yoffset: 0, width: 4, height: 3,
          expectation: gl.INVALID_OPERATION, message: "height is not a multiple of 4" },
        { xoffset: 0, yoffset: 0, width: 3, height: 4,
          expectation: gl.INVALID_OPERATION, message: "width is not a multiple of 4" },
        { xoffset: 1, yoffset: 0, width: 4, height: 4,
          expectation: gl.INVALID_OPERATION, message: "xoffset is not a multiple of 4" },
        { xoffset: 0, yoffset: 1, width: 4, height: 4,
          expectation: gl.INVALID_OPERATION, message: "yoffset is not a multiple of 4" },
        { xoffset: 12, yoffset: 12, width: 4, height: 4,
          expectation: gl.NO_ERROR, message: "is valid" },
    ]);

    if (contextVersion >= 2) {
        debug("");
        debug("Testing NPOT textures");
        ctu.testTexImageLevelDimensions(gl, ext, validFormats, expectedByteLength, getBlockDimensions,
          [
            { level: 0, width: 0, height: 0, expectation: gl.NO_ERROR, message: "0: 0x0 is valid" },
            { level: 0, width: 1, height: 1, expectation: gl.INVALID_OPERATION, message: "0: 1x1 is invalid" },
            { level: 0, width: 2, height: 2, expectation: gl.INVALID_OPERATION, message: "0: 2x2 is invalid" },
            { level: 0, width: 3, height: 3, expectation: gl.INVALID_OPERATION, message: "0: 3x3 is invalid" },
            { level: 0, width: 10, height: 10, expectation: gl.INVALID_OPERATION, message: "0: 10x10 is invalid" },
            { level: 0, width: 11, height: 11, expectation: gl.INVALID_OPERATION, message: "0: 11x11 is invalid" },
            { level: 0, width: 11, height: 12, expectation: gl.INVALID_OPERATION, message: "0: 11x12 is invalid" },
            { level: 0, width: 12, height: 11, expectation: gl.INVALID_OPERATION, message: "0: 12x11 is invalid" },
            { level: 0, width: 12, height: 12, expectation: gl.NO_ERROR, message: "0: 12x12 is valid" },
            { level: 1, width:  0, height:  0, expectation: gl.NO_ERROR, message: "1: 0x0, is valid" },
            { level: 1, width:  3, height:  3, expectation: gl.INVALID_OPERATION, message: "1: 3x3, is invalid" },
            { level: 1, width:  5, height:  5, expectation: gl.INVALID_OPERATION, message: "1: 5x5, is invalid" },
            { level: 1, width:  5, height:  6, expectation: gl.INVALID_OPERATION, message: "1: 5x6, is invalid" },
            { level: 1, width:  6, height:  5, expectation: gl.INVALID_OPERATION, message: "1: 6x5, is invalid" },
            { level: 1, width:  6, height:  6, expectation: gl.NO_ERROR, message: "1: 6x6, is valid" },
            { level: 2, width:  0, height:  0, expectation: gl.NO_ERROR, message: "2: 0x0, is valid" },
            { level: 2, width:  3, height:  3, expectation: gl.NO_ERROR, message: "2: 3x3, is valid" },
            { level: 3, width:  1, height:  3, expectation: gl.NO_ERROR, message: "3: 1x3, is valid" },
            { level: 3, width:  1, height:  1, expectation: gl.NO_ERROR, message: "3: 1x1, is valid" },
        ]);

        debug("");
        debug("Testing partial updates");
        ctu.testTexSubImageDimensions(gl, ext, validFormats, expectedByteLength, getBlockDimensions, 12, 12,
        [
          { xoffset: 0, yoffset: 0, width: 4, height: 3,
            expectation: gl.INVALID_OPERATION, message: "height is not a multiple of 4" },
          { xoffset: 0, yoffset: 0, width: 3, height: 4,
            expectation: gl.INVALID_OPERATION, message: "width is not a multiple of 4" },
          { xoffset: 1, yoffset: 0, width: 4, height: 4,
            expectation: gl.INVALID_OPERATION, message: "xoffset is not a multiple of 4" },
          { xoffset: 0, yoffset: 1, width: 4, height: 4,
            expectation: gl.INVALID_OPERATION, message: "yoffset is not a multiple of 4" },
          { xoffset: 8, yoffset: 8, width: 4, height: 4,
            expectation: gl.NO_ERROR, message: "is valid" },
        ]);

        debug("");
        debug("Testing immutable NPOT textures");
        ctu.testTexStorageLevelDimensions(gl, ext, validFormats, expectedByteLength, getBlockDimensions,
          [
            { width: 12, height: 12, expectation: gl.NO_ERROR, message: "0: 12x12 is valid" },
            { width:  6, height:  6, expectation: gl.NO_ERROR, message: "1: 6x6, is valid" },
            { width:  3, height:  3, expectation: gl.NO_ERROR, message: "2: 3x3, is valid" },
            { width:  1, height:  1, expectation: gl.NO_ERROR, message: "3: 1x1, is valid" },
        ]);
    }
}

function runTestRGTC() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 1,
            data: img_4x4_r_bc4,
            format: ext_rgtc.COMPRESSED_RED_RGTC1_EXT,
            hasAlpha: false,
        },
        {   width: 4,
            height: 4,
            channels: 1,
            data: img_4x4_signed_r_bc4,
            format: ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT,
            hasAlpha: false,
        },
        {   width: 4,
            height: 4,
            channels: 2,
            data: img_4x4_rg_bc5,
            format: ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT,
            hasAlpha: false,
        },
        {   width: 4,
            height: 4,
            channels: 2,
            data: img_4x4_signed_rg_bc5,
            format: ext_rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT,
            hasAlpha: false,
            error: 18, // Signed, so twice the normal error.
                       // Experimentally needed by e.g. RTX 3070.
        },
        {   width: 8,
            height: 8,
            channels: 2,
            data: img_8x8_r_bc4,
            format: ext_rgtc.COMPRESSED_RED_RGTC1_EXT,
            hasAlpha: false,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_r_bc4,
        },
        {   width: 8,
            height: 8,
            channels: 2,
            data: img_8x8_rg_bc5,
            format: ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT,
            hasAlpha: false,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rg_bc5,
        },
    ];
    testDXTTextures(tests);
}

function testDXT1_RGB() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 3,
            data: img_4x4_rgba_dxt1,
            format: ext.COMPRESSED_RGB_S3TC_DXT1_EXT,
            hasAlpha: false,
        },
        {   width: 8,
            height: 8,
            channels: 3,
            data: img_8x8_rgba_dxt1,
            format: ext.COMPRESSED_RGB_S3TC_DXT1_EXT,
            hasAlpha: false,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgba_dxt1
        }
    ];
    testDXTTextures(tests);
}

function testDXT1_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt1,
            format: ext.COMPRESSED_RGBA_S3TC_DXT1_EXT,
            // This is a special case -- the texture is still opaque
            // though it's RGBA.
            hasAlpha: false,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt1,
            format: ext.COMPRESSED_RGBA_S3TC_DXT1_EXT,
            // This is a special case -- the texture is still opaque
            // though it's RGBA.
            hasAlpha: false,
        }
    ];
    testDXTTextures(tests);
}

function testDXT3_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt3,
            format: ext.COMPRESSED_RGBA_S3TC_DXT3_EXT,
            hasAlpha: true,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt3,
            format: ext.COMPRESSED_RGBA_S3TC_DXT3_EXT,
            hasAlpha: true,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgba_dxt3
        }
    ];
    testDXTTextures(tests);
}

function testDXT5_RGBA() {
    var tests = [
        {   width: 4,
            height: 4,
            channels: 4,
            data: img_4x4_rgba_dxt5,
            format: ext.COMPRESSED_RGBA_S3TC_DXT5_EXT,
            hasAlpha: true,
        },
        {   width: 8,
            height: 8,
            channels: 4,
            data: img_8x8_rgba_dxt5,
            format: ext.COMPRESSED_RGBA_S3TC_DXT5_EXT,
            hasAlpha: true,
            subX0: 0,
            subY0: 0,
            subWidth: 4,
            subHeight: 4,
            subData: img_4x4_rgba_dxt5
        }
    ];
    testDXTTextures(tests);
}

function testDXTTextures(tests) {
    debug("<hr/>");
    for (var ii = 0; ii < tests.length; ++ii) {
        testDXTTexture(tests[ii], false);
        if (contextVersion >= 2) {
            debug("<br/>");
            testDXTTexture(tests[ii], true);
        }
    }
}

function uncompressDXTBlock(
    destBuffer, destX, destY, destWidth, src, srcOffset, format) {
    // Decoding routines follow D3D11 functional spec wrt
    // endpoints unquantization and interpolation.
    // Some hardware may produce slightly different values - it's normal.

    function make565(src, offset) {
        return src[offset + 0] + (src[offset + 1] << 8);
    }
    function make8888From565(c) {
        // These values exactly match hw decoder when selectors are 0 or 1.
        function replicateBits(v, w) {
           return (v << (8 - w)) | (v >> (w + w - 8));
        }
        return [
                replicateBits((c >> 11) & 0x1F, 5),
                replicateBits((c >>  5) & 0x3F, 6),
                replicateBits((c >>  0) & 0x1F, 5),
                255
            ];
    }
    function mix(mult, c0, c1, div) {
        var r = [];
        for (var ii = 0; ii < c0.length; ++ii) {
            // For green channel (6 bits), this interpolation exactly matches hw decoders

            // For red and blue channels (5 bits), this interpolation exactly
            // matches only some hw decoders and stays within acceptable range for others.
            r[ii] = Math.floor((c0[ii] * mult + c1[ii]) / div + 0.5);
        }
        return r;
    }
    var isBC45 = ext_rgtc &&
        (format == ext_rgtc.COMPRESSED_RED_RGTC1_EXT ||
         format == ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT ||
         format == ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT ||
         format == ext_rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT);
    let colorOffset = srcOffset;
    if (!isBC45) {
        var isDXT1 = format == ext.COMPRESSED_RGB_S3TC_DXT1_EXT ||
                     format == ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
        if (!isDXT1) {
            colorOffset += 8;
        }
        var color0 = make565(src, colorOffset + 0);
        var color1 = make565(src, colorOffset + 2);
        var c0gtc1 = color0 > color1 || !isDXT1;
        var rgba0 = make8888From565(color0);
        var rgba1 = make8888From565(color1);
        var colors = [
                rgba0,
                rgba1,
                c0gtc1 ? mix(2, rgba0, rgba1, 3) : mix(1, rgba0, rgba1, 2),
                c0gtc1 ? mix(2, rgba1, rgba0, 3) : [0, 0, 0, 255]
            ];
    }
    const isSigned = ext_rgtc && (format == ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT || format == ext_rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT);
    const signedSrc = new Int8Array(src);

    // yea I know there is a lot of math in this inner loop.
    // so sue me.
    for (var yy = 0; yy < 4; ++yy) {
        var pixels = src[colorOffset + 4 + yy];
        for (var xx = 0; xx < 4; ++xx) {
            var dstOff = ((destY + yy) * destWidth + destX + xx) * 4;
            if (!isBC45) {
                var code = (pixels >> (xx * 2)) & 0x3;
                var srcColor = colors[code];
            }
            var alpha;
            var rgChannel2 = 0;
            let decodeAlpha = (offset) => {
                let alpha;
                var alpha0 = (isSigned ? signedSrc : src)[offset + 0];
                var alpha1 = (isSigned ? signedSrc : src)[offset + 1];
                var alphaOff = (yy >> 1) * 3 + 2;
                var alphaBits =
                     src[offset + alphaOff + 0] +
                     src[offset + alphaOff + 1] * 256 +
                     src[offset + alphaOff + 2] * 65536;
                var alphaShift = (yy % 2) * 12 + xx * 3;
                var alphaCode = (alphaBits >> alphaShift) & 0x7;
                if (alpha0 > alpha1) {
                    switch (alphaCode) {
                    case 0:
                        alpha = alpha0;
                        break;
                    case 1:
                        alpha = alpha1;
                        break;
                    default:
                        alpha = Math.floor(((8 - alphaCode) * alpha0 + (alphaCode - 1) * alpha1) / 7.0 + 0.5);
                        break;
                    }
                } else {
                    switch (alphaCode) {
                    case 0:
                        alpha = alpha0;
                        break;
                    case 1:
                        alpha = alpha1;
                        break;
                    case 6:
                        alpha = 0;
                        break;
                    case 7:
                        alpha = 255;
                        break;
                    default:
                        alpha = Math.floor(((6 - alphaCode) * alpha0 + (alphaCode - 1) * alpha1) / 5.0 + 0.5);
                        break;
                    }
                }
                return alpha;
            }

            switch (format) {
            case ext.COMPRESSED_RGB_S3TC_DXT1_EXT:
                alpha = 255;
                break;
            case ext.COMPRESSED_RGBA_S3TC_DXT1_EXT:
                alpha = (code == 3 && !c0gtc1) ? 0 : 255;
                break;
            case ext.COMPRESSED_RGBA_S3TC_DXT3_EXT:
                {
                    var alpha0 = src[srcOffset + yy * 2 + (xx >> 1)];
                    var alpha1 = (alpha0 >> ((xx % 2) * 4)) & 0xF;
                    alpha = alpha1 | (alpha1 << 4);
                }
                break;
            case ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT:
            case ext_rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
                rgChannel2 = decodeAlpha(srcOffset + 8);
                // FALLTHROUGH
            case ext.COMPRESSED_RGBA_S3TC_DXT5_EXT:
            case ext_rgtc.COMPRESSED_RED_RGTC1_EXT:
            case ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT:
                alpha = decodeAlpha(srcOffset);
                break;
            default:
                throw "bad format";
            }
            if (isBC45) {
                destBuffer[dstOff + 0] = alpha;
                destBuffer[dstOff + 1] = rgChannel2;
                destBuffer[dstOff + 2] = 0;
                destBuffer[dstOff + 3] = 255;
                if (isSigned) {
                    destBuffer[dstOff + 0] = Math.max(0, alpha) * 2;
                    destBuffer[dstOff + 1] = Math.max(0, rgChannel2) * 2;
                }
            } else {
                destBuffer[dstOff + 0] = srcColor[0];
                destBuffer[dstOff + 1] = srcColor[1];
                destBuffer[dstOff + 2] = srcColor[2];
                destBuffer[dstOff + 3] = alpha;
            }
        }
    }
}

function getBlockSize(format) {
  var isDXT1 = format == ext.COMPRESSED_RGB_S3TC_DXT1_EXT ||
               format == ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
  var isBC4 = ext_rgtc && (format == ext_rgtc.COMPRESSED_RED_RGTC1_EXT || format == ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT);
  return isDXT1 || isBC4 ? 8 : 16;
}

function uncompressDXT(width, height, data, format) {
    if (width % 4 || height % 4) throw "bad width or height";

    var dest = new Uint8Array(width * height * 4);
    var blocksAcross = width / 4;
    var blocksDown = height / 4;
    var blockSize = getBlockSize(format);
    for (var yy = 0; yy < blocksDown; ++yy) {
        for (var xx = 0; xx < blocksAcross; ++xx) {
            uncompressDXTBlock(
                dest, xx * 4, yy * 4, width, data,
                (yy * blocksAcross + xx) * blockSize, format);
        }
    }
    return dest;
}

function uncompressDXTIntoSubRegion(width, height, subX0, subY0, subWidth, subHeight, data, format)
{
    if (width % 4 || height % 4 || subX0 % 4 || subY0 % 4 || subWidth % 4 || subHeight % 4)
        throw "bad dimension";

    var dest = new Uint8Array(width * height * 4);
    // Zero-filled DXT1 or BC4/5 texture represents [0, 0, 0, 255]
    if (format == ext.COMPRESSED_RGB_S3TC_DXT1_EXT || format == ext.COMPRESSED_RGBA_S3TC_DXT1_EXT ||
        format == ext.COMPRESSED_RED_RGTC1_EXT || format == ext.COMPRESSED_SIGNED_RED_RGTC1_EXT ||
        format == ext.COMPRESSED_RED_GREEN_RGTC2_EXT || format == ext.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT) {
        for (var i = 3; i < dest.length; i += 4) dest[i] = 255;
    }
    var blocksAcross = subWidth / 4;
    var blocksDown = subHeight / 4;
    var blockSize = getBlockSize(format);
    for (var yy = 0; yy < blocksDown; ++yy) {
        for (var xx = 0; xx < blocksAcross; ++xx) {
            uncompressDXTBlock(
                dest, subX0 + xx * 4, subY0 + yy * 4, width, data,
                (yy * blocksAcross + xx) * blockSize, format);
        }
    }
    return dest;
}

function copyRect(data, srcX, srcY, dstX, dstY, width, height, stride) {
  var bytesPerLine = width * 4;
  var srcOffset = srcX * 4 + srcY * stride;
  var dstOffset = dstX * 4 + dstY * stride;
  for (; height > 0; --height) {
    for (var ii = 0; ii < bytesPerLine; ++ii) {
      data[dstOffset + ii] = data[srcOffset + ii];
    }
    srcOffset += stride;
    dstOffset += stride;
  }
}

function testDXTTexture(test, useTexStorage) {
    test.error = test.error || DEFAULT_COLOR_ERROR;

    var data = new Uint8Array(test.data);
    var width = test.width;
    var height = test.height;
    var format = test.format;

    var uncompressedData = uncompressDXT(width, height, data, format);

    canvas.width = width;
    canvas.height = height;
    gl.viewport(0, 0, width, height);
    debug("testing " + ctu.formatToString(ext, format) + " " + width + "x" + height +
          (useTexStorage ? " via texStorage2D" : " via compressedTexImage2D"));

    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    if (useTexStorage) {
        if (test.subData) {
            var uncompressedDataSub = uncompressDXTIntoSubRegion(
                width, height, test.subX0, test.subY0, test.subWidth, test.subHeight, test.subData, format);
            var tex1 = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, tex1);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

            gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "allocating compressed texture via texStorage2D");
            gl.compressedTexSubImage2D(
                gl.TEXTURE_2D, 0, test.subX0, test.subY0, test.subWidth, test.subHeight, format, test.subData);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture data via compressedTexSubImage2D");

            wtu.clearAndDrawUnitQuad(gl);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 1");
            compareRect(width, height, test.channels, uncompressedDataSub, "NEAREST", test.error);

            // Clean up and recover
            gl.deleteTexture(tex1);
            gl.bindTexture(gl.TEXTURE_2D, tex);
        }

        gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "allocating compressed texture via texStorage2D");
        wtu.clearAndDrawUnitQuad(gl);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
        var clearColor = (test.hasAlpha ? [0, 0, 0, 0] : [0, 0, 0, 255]);
        wtu.checkCanvas(gl, clearColor, "texture should be initialized to black");
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, data);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture data via compressedTexSubImage2D");
    } else {
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture");
    }
    gl.generateMipmap(gl.TEXTURE_2D);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "trying to generate mipmaps from compressed texture");
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "after clearing generateMipmap error");
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 1");
    compareRect(width, height, test.channels, uncompressedData, "NEAREST", test.error);
    // Test again with linear filtering.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad 2");
    compareRect(width, height, test.channels, uncompressedData, "LINEAR", test.error);

    if (!useTexStorage) {
        // It's not allowed to redefine textures defined via texStorage2D.
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height, 1, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "non 0 border");

        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width + 4, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height + 4, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 4, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 4, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");

        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 1, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width - 2, height, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 1, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
        gl.compressedTexImage2D(gl.TEXTURE_2D, 0, format, width, height - 2, 0, data);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");

        if (width == 4) {
            // The width/height of the implied base level must be a multiple of the block size.
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, 1, height, 0, data);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions for level > 0");
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, 2, height, 0, data);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "valid dimensions for level > 0");
        }
        if (height == 4) {
            // The width/height of the implied base level must be a multiple of the block size.
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, width, 1, 0, data);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions for level > 0");
            gl.compressedTexImage2D(gl.TEXTURE_2D, 1, format, width, 2, 0, data);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "valid dimensions for level > 0");
        }
    }

    // pick a wrong format that uses the same amount of data.
    var wrongFormat;
    switch (format) {
    case ext.COMPRESSED_RGB_S3TC_DXT1_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT1_EXT:
      wrongFormat = ext.COMPRESSED_RGB_S3TC_DXT1_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT3_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
      break;
    case ext.COMPRESSED_RGBA_S3TC_DXT5_EXT:
      wrongFormat = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
      break;
    case ext_rgtc.COMPRESSED_RED_RGTC1_EXT:
    case ext_rgtc.COMPRESSED_SIGNED_RED_RGTC1_EXT:
      wrongFormat = ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT;
      break;
    case ext_rgtc.COMPRESSED_RED_GREEN_RGTC2_EXT:
    case ext_rgtc.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
      wrongFormat = ext_rgtc.COMPRESSED_RED_RGTC1_EXT;
      break;
    }

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, wrongFormat, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "format does not match");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 4, 0, width, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "dimension out of range");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 4, width, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "dimension out of range");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width + 4, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height + 4, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 4, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 4, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "data size does not match dimensions");

    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 1, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width - 2, height, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 1, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height - 2, format, data);
    wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid dimensions");

    var subData = new Uint8Array(data.buffer, 0, getBlockSize(format));

    if (width == 8 && height == 8) {
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 1, 0, 4, 4, format, subData);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid offset");
        gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 1, 4, 4, format, subData);
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "invalid offset");
    }

    var stride = width * 4;
    for (var yoff = 0; yoff < height; yoff += 4) {
        for (var xoff = 0; xoff < width; xoff += 4) {
            copyRect(uncompressedData, 0, 0, xoff, yoff, 4, 4, stride);
            gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, xoff, yoff, 4, 4, format, subData);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading compressed texture");
            // First test NEAREST filtering.
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
            wtu.clearAndDrawUnitQuad(gl);
            compareRect(width, height, test.channels, uncompressedData, "NEAREST", test.error);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
            // Next test LINEAR filtering.
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            wtu.clearAndDrawUnitQuad(gl);
            wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
            compareRect(width, height, test.channels, uncompressedData, "LINEAR", test.error);
        }
    }
}

function testDXT5_RGBA_PBO() {
    debug("");
    debug("testing PBO uploads");
    var width = 8;
    var height = 8;
    var channels = 4;
    var data = img_8x8_rgba_dxt5;
    var format = ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
    var uncompressedData = uncompressDXT(width, height, data, format);

    var tex = gl.createTexture();

    // First, PBO size = image size
    var pbo1 = gl.createBuffer();
    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo1);
    gl.bufferData(gl.PIXEL_UNPACK_BUFFER, data, gl.STATIC_DRAW);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading a PBO");

    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texStorage2D(gl.TEXTURE_2D, 1, format, width, height);
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, data.length, 0);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading a texture from a PBO");

    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
    compareRect(width, height, channels, uncompressedData, "NEAREST", DEFAULT_COLOR_ERROR);

    // Clear the texture before the next test
    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, new Uint8Array(data.length));

    // Second, image is just a subrange of the PBO
    var pbo2 = gl.createBuffer();
    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo2);
    gl.bufferData(gl.PIXEL_UNPACK_BUFFER, data.length*3, gl.STATIC_DRAW);
    gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, data.length, data);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading a PBO subrange");
    gl.compressedTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, format, data.length, data.length);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "uploading a texture from a PBO subrange");
    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
    wtu.clearAndDrawUnitQuad(gl);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "drawing unit quad");
    compareRect(width, height, channels, uncompressedData, "NEAREST", DEFAULT_COLOR_ERROR);
}

function compareRect(width, height, channels, expectedData, filteringMode, colorError) {
    var actual = new Uint8Array(width * height * 4);
    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, actual);
    wtu.glErrorShouldBe(gl, gl.NO_ERROR, "reading back pixels");

    var div = document.createElement("div");
    div.className = "testimages";
    ctu.insertCaptionedImg(div, "expected", ctu.makeScaledImage(width, height, width, expectedData, true));
    ctu.insertCaptionedImg(div, "actual", ctu.makeScaledImage(width, height, width, actual, true));
    div.appendChild(document.createElement('br'));
    document.getElementById("console").appendChild(div);

    var failed = false;
    for (var yy = 0; yy < height; ++yy) {
        for (var xx = 0; xx < width; ++xx) {
            var offset = (yy * width + xx) * 4;
            var expected = expectedData.slice(offset, offset + 4);
            const was = actual.slice(offset, offset + 4);

            // Compare RGB values
            for (var jj = 0; jj < 3; ++jj) {
                if (Math.abs(was[jj] - expected[jj]) > colorError) {
                    failed = true;
                    testFailed(`RGB at (${xx}, ${yy}) expected: ${expected}` +
                               ` +/- ${colorError}, was ${was}`);
                    break;
                }
            }

            if (channels == 3) {
                // BC1 RGB is allowed to be mapped to BC1 RGBA.
                // In such a case, 3-color mode black value can be transparent:
                // [0, 0, 0, 0] instead of [0, 0, 0, 255].

                if (actual[offset + 3] != expected[3]) {
                    // Got non-opaque value for opaque format

                    // Check RGB values. Notice, that the condition here
                    // is more permissive than needed since we don't have
                    // compressed data at this point.
                    if (was[0] == 0 &&
                        was[1] == 0 &&
                        was[2] == 0 &&
                        was[3] == 0) {
                            debug("<b>DXT1 RGB is mapped to DXT1 RGBA</b>");
                    } else {
                        failed = true;
                        testFailed('Alpha at (' + xx + ', ' + yy +
                                              ') expected: ' + expected[3] + ' was ' + was);
                    }
                }
            } else {
                // Compare Alpha values
                // Acceptable interpolation error depends on endpoints:
                // 1.0 / 65535.0 + 0.03 * max(abs(endpoint0 - endpoint1), abs(endpoint0_p - endpoint1_p))
                // For simplicity, assume the worst case (e0 is 0.0, e1 is 1.0). After conversion to unorm8, it is 8.
                if (Math.abs(was[3] - expected[3]) > 8) {
                    failed = true;
                    testFailed('Alpha at (' + xx + ', ' + yy +
                                          ') expected: ' + expected + ' +/- 8 was ' + was);
                }
            }
        }
    }
    if (!failed) {
        testPassed("texture rendered correctly with " + filteringMode + " filtering");
    }
}

debug("");
var successfullyParsed = true;
</script>
<script src="../../js/js-test-post.js"></script>

</body>
</html>
